/*
 * Copyright 2016 _that/TeamWin
 * This file is part of TWRP/TeamWin Recovery Project.
 *
 * TWRP is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * TWRP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with TWRP.  If not, see <http://www.gnu.org/licenses/>.
 */

// terminal.cpp - GUITerminal object

#include "gui/terminal.hpp"

#include <vector>

#include <cstring>

#include <fcntl.h>
#include <linux/input.h>
#include <sys/wait.h>
#include <termio.h>
#include <unistd.h>

#include "mblog/logging.h"

#include "data.hpp"
#include "variables.h"

#define LOG_TAG "mbbootui/gui/terminal"

#if 0
#define debug_printf printf
#else
#define debug_printf(...)
#endif

extern int g_pty_fd; // in gui.cpp where the select is

/*
Pseudoterminal handler.
*/
class Pseudoterminal
{
public:
    Pseudoterminal() : fdMaster(0), pid(0)
    {
    }

    bool started() const {
        return pid > 0;
    }

    bool start()
    {
        fdMaster = getpt();
        if (fdMaster < 0) {
            LOGE("Error %d on getpt()", errno);
            return false;
        }

        if (unlockpt(fdMaster) != 0) {
            LOGE("Error %d on unlockpt()", errno);
            return false;
        }

        pid = fork();
        if (pid < 0) {
            LOGE("fork failed for pty, error %d", errno);
            close(fdMaster);
            pid = 0;
            return false;
        } else if (pid) {
            // child started, now someone needs to periodically read from fdMaster
            // and write it to the terminal
            // this currently works through gui.cpp calling terminal_pty_read below
            g_pty_fd = fdMaster;
            return true;
        } else {
            // O_CLOEXEC should not be used
            int fdSlave = open(ptsname(fdMaster), O_RDWR);
            close(fdMaster);
            runSlave(fdSlave);
        }
        // we can't get here
        LOGE("impossible error in pty");
        return false;
    }

    void runSlave(int fdSlave)
    {
        dup2(fdSlave, STDIN_FILENO); // PTY becomes standard input (0)
        dup2(fdSlave, STDOUT_FILENO); // PTY becomes standard output (1)
        dup2(fdSlave, STDERR_FILENO); // PTY becomes standard error (2)

        // Now the original file descriptor is useless
        close(fdSlave);

        // Make the current process a new session leader
        if (setsid() == (pid_t) -1) {
            LOGE("setsid failed: %d", errno);
        }

        // As the child is a session leader, set the controlling terminal to be the slave side of the PTY
        // (Mandatory for programs like the shell to make them manage correctly their outputs)
        ioctl(0, TIOCSCTTY, 1);

        execl("/sbin/sh", "sh", nullptr);
        _exit(127);
    }

    int read(char* buffer, size_t size)
    {
        if (!started()) {
            LOGE("someone tried to read from pty, but it was not started");
            return -1;
        }
        int rc = ::read(fdMaster, buffer, size);
        debug_printf("pty read: %d bytes\n", rc);
        if (rc < 0) {
            // assume child has died (usual errno when shell exits seems to be EIO == 5)
            if (errno != EIO) {
                LOGE("pty read failed: %d", errno);
            }
            stop();
        }
        return rc;
    }

    int write(const char* buffer, size_t size)
    {
        if (!started()) {
            LOGE("someone tried to write to pty, but it was not started");
            return -1;
        }
        int rc = ::write(fdMaster, buffer, size);
        debug_printf("pty write: %d bytes -> %d\n", size, rc);
        if (rc < 0) {
            LOGE("pty write failed: %d", errno);
            // assume child has died
            stop();
        }
        return rc;
    }

    template<size_t n>
    inline int write(const char (&literal)[n])
    {
        return write(literal, n - 1);
    }

    void resize(int xChars, int yChars, int w, int h)
    {
        struct winsize ws;
        ws.ws_row = yChars;
        ws.ws_col = xChars;
        ws.ws_xpixel = w;
        ws.ws_ypixel = h;
        if (ioctl(fdMaster, TIOCSWINSZ, &ws) < 0) {
            LOGE("failed to set window size, error %d", errno);
        }
    }

    void stop()
    {
        if (!started()) {
            LOGE("someone tried to stop pty, but it was not started");
            return;
        }
        close(fdMaster);
        g_pty_fd = fdMaster = -1;
        int status;
        // avoid zombies but don't hang if the child is still alive and we got
        // here due to some error
        waitpid(pid, &status, WNOHANG);
        pid = 0;
    }

private:
    int fdMaster;
    pid_t pid;
};

// UTF-8 decoder
// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
// See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.

const uint32_t UTF8_ACCEPT = 0;
const uint32_t UTF8_REJECT = 1;

static const uint8_t utf8d[] = {
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
    7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
    8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
    0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
    0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
    0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
    1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
    1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
    1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
};

uint32_t inline utf8decode(uint32_t* state, uint32_t* codep, uint32_t byte)
{
    uint32_t type = utf8d[byte];

    *codep = (*state != UTF8_ACCEPT)
            ? (byte & 0x3fu) | (*codep << 6)
            : (0xff >> type) & (byte);

    *state = utf8d[256 + *state*16 + type];
    return *state;
}
// end of UTF-8 decoder

// Append a UTF-8 codepoint to string s
size_t utf8add(std::string& s, uint32_t cp)
{
    if (cp < 0x7f) {
        s += cp;
        return 1;
    } else if (cp < 0x7ff) {
        s += (0xc0 | (cp >> 6));
        s += (0x80 | (cp & 0x3f));
        return 2;
    } else if (cp < 0xffff) {
        s += (0xe0 | (cp >> 12));
        s += (0x80 | ((cp >> 6) & 0x3f));
        s += (0x80 | (cp & 0x3f));
        return 3;
    } else if (cp < 0x1fffff) {
        s += (0xf0 | (cp >> 18));
        s += (0x80 | ((cp >> 12) & 0x3f));
        s += (0x80 | ((cp >> 6) & 0x3f));
        s += (0x80 | (cp & 0x3f));
        return 4;
    }
    return 0;
}

/*
TerminalEngine is the terminal back-end, dealing with the text buffer and attributes
and with communicating with the pty.
It does not care about visual things like rendering, fonts, windows etc.
The idea is that 0 to n GUITerminal instances (e.g. on different pages) can connect
to one TerminalEngine to interact with the terminal, and that the TerminalEngine
survives things like page changes or even theme reloads.
*/
class TerminalEngine
{
public:
#if 0 // later
    struct Attributes
    {
        COLOR fgcolor; // TODO: what about palette?
        COLOR bgcolor;
        // could add bold, underline, blink, etc.
    };

    struct AttributeRange
    {
        size_t start; // start position inside text (in bytes)
        Attributes a;
    };
#endif
    typedef uint32_t CodePoint; // Unicode code point

    // A line of text, optimized for rendering and storage in the buffer
    struct Line
    {
        std::string text; // in UTF-8 format
        //std::vector<AttributeRange> attrs;

        Line() {}

        size_t utf8forward(size_t start) const
        {
            if (start >= text.size()) {
                return start;
            }
            uint32_t u8state = 0, u8cp = 0;
            size_t i = start;
            uint32_t rc;
            do {
                rc = utf8decode(&u8state, &u8cp, (unsigned char) text[i]);
                ++i;
            } while (rc != UTF8_ACCEPT && rc != UTF8_REJECT && i < text.size());
            return i;
        }

        std::string substr(size_t start, size_t n) const
        {
            size_t i = 0;
            for (; start && i < text.size(); i = utf8forward(i)) {
                --start;
            }
            size_t s = i;
            for (; n && i < text.size(); i = utf8forward(i)) {
                --n;
            }
            return text.substr(s, i - s);
        }

        size_t length() const
        {
            size_t n = 0;
            for (size_t i = 0; i < text.size(); i = utf8forward(i)) {
                ++n;
            }
            return n;
        }
    };

    // A single character cell with a Unicode code point
    struct Cell
    {
        Cell() : cp(' ') {}
        Cell(CodePoint cp) : cp(cp) {}
        CodePoint cp;
        //Attributes a;
    };

    // A line of text, optimized for editing single characters
    struct UnpackedLine
    {
        std::vector<Cell> cells;

        void eraseFrom(size_t x)
        {
            if (cells.size() > x) {
                cells.erase(cells.begin() + x, cells.end());
            }
        }

        void eraseTo(size_t x)
        {
            if (x > 0) {
                cells.erase(cells.begin(), cells.begin() + x);
            }
        }
    };

    TerminalEngine()
    {
        // the default size will be overwritten by the GUI window when the size is known
        width = 40;
        height = 10;

        clear();
        updateCounter = 0;
        state = kStateGround;
        utf8state = utf8codepoint = 0;
    }

    void setSize(int xChars, int yChars, int w, int h)
    {
        width = xChars;
        height = yChars;
        if (pty.started()) {
            pty.resize(width, height, w, h);
        }
        debug_printf("setSize: %d*%d chars, %d*%d pixels\n",
                     xChars, yChars, w, h);
    }

    void initPty()
    {
        if (!pty.started()) {
            pty.start();
            pty.resize(width, height, 0, 0);
        }
    }

    void readPty()
    {
        char buffer[1024];
        int rc = pty.read(buffer, sizeof(buffer));
        debug_printf("readPty: %d bytes\n", rc);
        if (rc < 0) {
            output("\r\nChild process exited.\r\n");
            // TODO: maybe exit terminal here
        } else {
            for (int i = 0; i < rc; ++i) {
                output(buffer[i]);
            }
        }
    }

    void clear()
    {
        cursorX = cursorY = 0;
        lines.clear();
        setY(0);
        unpackLine(0);
        ++updateCounter;
    }

    void output(const char *buf)
    {
        for (const char* p = buf; *p; ++p) {
            output(*p);
        }
    }

    void output(const char ch)
    {
        char debug[2]; debug[0] = ch; debug[1] = 0;
        debug_printf("output: %d %s\n", (int) ch,
                     (ch >= ' ' && ch < 127) ? debug : ch == 27 ? "esc" : "");
        if (ch < 32) {
            // always process control chars, even after incomplete UTF-8 fragments
            processC0(ch);
            if (utf8state != UTF8_ACCEPT) {
                debug_printf("Terminal: incomplete UTF-8 fragment before "
                             "control char ignored, codepoint=%u ch=%d\n",
                             utf8codepoint, (int) ch);
                utf8state = UTF8_ACCEPT;
            }
            return;
        }
        uint32_t rc = utf8decode(&utf8state, &utf8codepoint, (unsigned char) ch);
        if (rc == UTF8_ACCEPT) {
            processCodePoint(utf8codepoint);
        } else if (rc == UTF8_REJECT) {
            debug_printf("Terminal: invalid UTF-8 sequence ignored, "
                         "codepoint=%u ch=%d\n", utf8codepoint, (int) ch);
            utf8state = UTF8_ACCEPT;
        }
        // else we need to read more bytes to assemble a codepoint
    }

    bool inputChar(int ch)
    {
        debug_printf("inputChar: %d\n", ch);
        initPty(); // reinit just in case it died before
        // encode the char as UTF-8 and send it to the pty
        std::string c;
        utf8add(c, (uint32_t) ch);
        pty.write(c.c_str(), c.size());
        return true;
    }

    bool inputKey(int key)
    {
        debug_printf("inputKey: %d\n", key);
        switch (key) {
            case KEY_UP: pty.write("\e[A"); break;
            case KEY_DOWN: pty.write("\e[B"); break;
            case KEY_RIGHT: pty.write("\e[C"); break;
            case KEY_LEFT: pty.write("\e[D"); break;
            case KEY_HOME: pty.write("\eOH"); break;
            case KEY_END: pty.write("\eOF"); break;
            case KEY_INSERT: pty.write("\e[2~"); break;
            case KEY_DELETE: pty.write("\e[3~"); break;
            case KEY_PAGEUP: pty.write("\e[5~"); break;
            case KEY_PAGEDOWN: pty.write("\e[6~"); break;
            // TODO: other keys
            default:
                return false;
        }
        return true;
    }

    size_t getLinesCount() const
    {
        return lines.size();
    }

    const Line& getLine(size_t n)
    {
        if (unpackedY == n) {
            packLine();
        }
        return lines[n];
    }

    int getCursorX() const
    {
        return cursorX;
    }

    int getCursorY() const
    {
        return cursorY;
    }

    int getUpdateCounter() const
    {
        return updateCounter;
    }

    void setX(int x)
    {
        x = std::min(width, std::max(x, 0));
        cursorX = x;
        ++updateCounter;
    }

    void setY(int y)
    {
        //y = min(height, max(y, 0));
        y = std::max(y, 0);
        cursorY = y;
        while (lines.size() <= (size_t) y) {
            lines.push_back(Line());
        }
        ++updateCounter;
    }

    void up(int n = 1)
    {
        setY(cursorY - n);
    }

    void down(int n = 1)
    {
        setY(cursorY + n);
    }

    void left(int n = 1)
    {
        setX(cursorX - n);
    }

    void right(int n = 1)
    {
        setX(cursorX + n);
    }

private:
    void packLine()
    {
        std::string& s = lines[unpackedY].text;
        s.clear();
        for (size_t i = 0; i < unpackedLine.cells.size(); ++i) {
            Cell& c = unpackedLine.cells[i];
            utf8add(s, c.cp);
            // later: if attributes changed, add attributes
        }
    }

    void unpackLine(size_t y)
    {
        uint32_t u8state = 0, u8cp = 0;
        std::string& s = lines[y].text;
        unpackedLine.cells.clear();
        for (size_t i = 0; i < s.size(); ++i) {
            uint32_t rc = utf8decode(&u8state, &u8cp, (unsigned char) s[i]);
            if (rc == UTF8_ACCEPT) {
                unpackedLine.cells.push_back(Cell(u8cp));
            }
        }
        if (unpackedLine.cells.size() < (size_t) width) {
            unpackedLine.cells.resize(width);
        }
        unpackedY = y;
    }

    void ensureUnpacked(size_t y)
    {
        if (unpackedY != y) {
            packLine();
            unpackLine(y);
        }
    }

    void processC0(char ch)
    {
        switch (ch) {
        case 7: // BEL
            DataManager::Vibrate(VAR_TW_BUTTON_VIBRATE);
            break;
        case 8: // BS
            left();
            break;
        case 9: // HT
            // TODO: this might be totally wrong
            right();
            while (cursorX % 8 != 0 && cursorX < width) {
                right();
            }
            break;
        case 10: // LF
        case 11: // VT
        case 12: // FF
            down();
            break;
        case 13: // CR
            setX(0);
            break;
        case 24: // CAN
        case 26: // SUB
            state = kStateGround;
            ctlseq.clear();
            break;
        case 27: // ESC
            state = kStateEsc;
            ctlseq.clear();
            break;
        }
    }

    void processCodePoint(CodePoint cp)
    {
        ++updateCounter;
        debug_printf("codepoint: %u\n", cp);
        if (cp == 0x9b) { // CSI
            state = kStateCsi;
            ctlseq.clear();
            return;
        }
        switch (state) {
        case kStateGround:
            processChar(cp);
            break;
        case kStateEsc:
            processEsc(cp);
            break;
        case kStateCsi:
            processControlSequence(cp);
            break;
        }
    }

    void processChar(CodePoint cp)
    {
        ensureUnpacked(cursorY);
        // extend unpackedLine if needed, write ch into cell
        if (unpackedLine.cells.size() <= (size_t) cursorX) {
            unpackedLine.cells.resize(cursorX + 1);
        }
        unpackedLine.cells[cursorX].cp = cp;

        right();
        if (cursorX >= width) {
            // TODO: configurable line wrapping
            // TODO: don't go down immediately but only on next char?
            down();
            setX(0);
        }
        // TODO: update all GUI objects that display this terminal engine
    }

    void processEsc(CodePoint cp)
    {
        switch (cp) {
        case 'c': // TODO: Reset
            break;
        case 'D': // Line feed
            down();
            break;
        case 'E': // Newline
            setX(0);
            down();
            break;
        case '[': // CSI
            state = kStateCsi;
            ctlseq.clear();
            break;
        case ']': // TODO: OSC state
        default:
            state = kStateGround;
        }
    }

    void processControlSequence(CodePoint cp)
    {
        if (cp >= 0x40 && cp <= 0x7e) {
            ctlseq += cp;
            execControlSequence(ctlseq);
            ctlseq.clear();
            state = kStateGround;
            return;
        }
        if (isdigit(cp) || cp == ';' /* || (ch >= 0x3c && ch <= 0x3f) */) {
            ctlseq += cp;
            // state = kStateCsiParam;
            return;
        }
    }

    static int parseArg(std::string& s, int defaultvalue)
    {
        if (s.empty() || !isdigit(s[0])) {
            return defaultvalue;
        }
        int value = atoi(s.c_str());
        size_t pos = s.find(';');
        s.erase(0, pos != std::string::npos ? pos + 1 : std::string::npos);
        return value;
    }

    void execControlSequence(std::string ctlseq)
    {
        // assert(!ctlseq.empty());
        if (ctlseq == "6n") {
            // CPR - cursor position report
            char answer[20];
            sprintf(answer, "\e[%d;%dR", cursorY, cursorX);
            pty.write(answer, strlen(answer));
            return;
        }
        char f = *ctlseq.rbegin();
        // if (f == '?') ... private mode
        switch (f) {
        // case '@': // ICH - insert character
        case 'A': // CUU - cursor up
            up(parseArg(ctlseq, 1));
            break;
        case 'B': // CUD - cursor down
        case 'e': // VPR - line position forward
            down(parseArg(ctlseq, 1));
            break;
        case 'C': // CUF - cursor right
        case 'a': // HPR - character position forward
            right(parseArg(ctlseq, 1));
            break;
        case 'D': // CUB - cursor left
            left(parseArg(ctlseq, 1));
            break;
        case 'E': // CNL - cursor next line
            down(parseArg(ctlseq, 1));
            setX(0);
            break;
        case 'F': // CPL - cursor preceding line
            up(parseArg(ctlseq, 1));
            setX(0);
            break;
        case 'G': // CHA - cursor character absolute
            setX(parseArg(ctlseq, 1)-1);
            break;
        case 'H': // CUP - cursor position
            // TODO: consider scrollback area
            setY(parseArg(ctlseq, 1)-1);
            setX(parseArg(ctlseq, 1)-1);
            break;
        case 'J': { // ED - erase in page
            int param = parseArg(ctlseq, 0);
            ensureUnpacked(cursorY);
            switch (param) {
            default:
            case 0:
                unpackedLine.eraseFrom(cursorX);
                if (lines.size() > (size_t) cursorY + 1) {
                    lines.erase(lines.begin() + cursorY + 1, lines.end());
                }
                break;
            case 1:
                unpackedLine.eraseTo(cursorX);
                if (cursorY > 0) {
                    lines.erase(lines.begin(), lines.begin() + cursorY - 1);
                    cursorY = 0;
                }
                break;
            case 2: // clear
            case 3: // clear incl scrollback
                clear();
                break;
            }
            break;
        }
        case 'K': { // EL - erase in line
            int param = parseArg(ctlseq, 0);
            ensureUnpacked(cursorY);
            switch (param) {
            default:
            case 0:
                unpackedLine.eraseFrom(cursorX);
                break;
            case 1:
                unpackedLine.eraseTo(cursorX);
                break;
            case 2:
                unpackedLine.cells.clear();
                break;
            }
            break;
        }
        // case 'L': // IL - insert line

        default:
            debug_printf("unknown ctlseq: '%s'\n", ctlseq.c_str());
            break;
        }
    }

private:
    int cursorX, cursorY; // 0-based, char based. TODO: decide how to handle scrollback
    int width, height; // window size in chars
    std::vector<Line> lines; // the text buffer
    UnpackedLine unpackedLine; // current line for editing
    size_t unpackedY; // number of current line
    int updateCounter; // changes whenever terminal could require redraw

    Pseudoterminal pty;
    enum { kStateGround, kStateEsc, kStateCsi } state;

    // for accumulating a full UTF-8 character from individual bytes
    uint32_t utf8state;
    uint32_t utf8codepoint;

    // for accumulating a control sequence after receiving CSI
    std::string ctlseq;
};

// The one and only terminal engine for now
TerminalEngine gEngine;

void terminal_pty_read()
{
    gEngine.readPty();
}


GUITerminal::GUITerminal(xml_node<>* node) : GUIScrollList(node)
{
    allowSelection = false; // terminal doesn't support list item selections
    lastCondition = false;

    if (!node) {
        mRenderX = 0;
        mRenderY = 0;
        mRenderW = gr_fb_width();
        mRenderH = gr_fb_height();
    }

    engine = &gEngine;
    updateCounter = 0;
}

int GUITerminal::Update()
{
    if (!isConditionTrue()) {
        lastCondition = false;
        return 0;
    }

    if (lastCondition == false) {
        lastCondition = true;
        // we're becoming visible, so we might need to resize the terminal content
        InitAndResize();
    }

    if (updateCounter != engine->getUpdateCounter()) {
        // try to keep the cursor in view
        SetVisibleListLocation(engine->getCursorY());
        updateCounter = engine->getUpdateCounter();
    }

    GUIScrollList::Update();

    if (mUpdate) {
        mUpdate = 0;
        if (Render() == 0) {
            return 2;
        }
    }
    return 0;
}

// NotifyTouch - Notify of a touch event
//  Return 0 on success, >0 to ignore remainder of touch, and <0 on error
int GUITerminal::NotifyTouch(TOUCH_STATE state, int x, int y)
{
    if (!isConditionTrue()) {
        return -1;
    }

    // TODO: grab focus correctly
    // TODO: fix focus handling in PageManager and GUIInput
    SetInputFocus(1);
    debug_printf("Terminal: SetInputFocus\n");
    return GUIScrollList::NotifyTouch(state, x, y);
    // TODO later: allow cursor positioning by touch (simulate mouse click?)
    // http://stackoverflow.com/questions/5966903/how-to-get-mousemove-and-mouseclick-in-bash
    // will likely not work with Busybox anyway
}

int GUITerminal::NotifyKey(int key, bool down)
{
    if (!HasInputFocus) {
        return 1;
    }
    if (down) {
        if (engine->inputKey(key)) {
            mUpdate = 1;
        }
    }
    return 0;
}

// character input
int GUITerminal::NotifyCharInput(int ch)
{
    if (engine->inputChar(ch)) {
        mUpdate = 1;
    }
    return 0;
}

size_t GUITerminal::GetItemCount()
{
    return engine->getLinesCount();
}

void GUITerminal::RenderItem(size_t itemindex, int yPos, bool selected __unused)
{
    const TerminalEngine::Line& line = engine->getLine(itemindex);

    gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, mFontColor.alpha);
    // later: handle attributes here

    // render text
    const char* text = line.text.c_str();
    gr_textEx_scaleW(mRenderX, yPos, text, mFont->GetResource(), mRenderW, TOP_LEFT, 0);

    if (itemindex == (size_t) engine->getCursorY()) {
        // render cursor
        int cursorX = engine->getCursorX();
        std::string leftOfCursor = line.substr(0, cursorX);
        int x = gr_ttf_measureEx(leftOfCursor.c_str(), mFont->GetResource());
        // note that this single character can be a UTF-8 sequence
        std::string atCursor = (size_t) cursorX < line.length() ? line.substr(cursorX, 1) : " ";
        int w = gr_ttf_measureEx(atCursor.c_str(), mFont->GetResource());
        gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, mFontColor.alpha);
        gr_fill(mRenderX + x, yPos, w, actualItemHeight);
        gr_color(mBackgroundColor.red, mBackgroundColor.green, mBackgroundColor.blue, mBackgroundColor.alpha);
        gr_textEx_scaleW(mRenderX + x, yPos, atCursor.c_str(), mFont->GetResource(), mRenderW, TOP_LEFT, 0);
    }
}

void GUITerminal::NotifySelect(size_t item_selected __unused)
{
    // do nothing - terminal ignores selections
}

void GUITerminal::InitAndResize()
{
    // make sure the shell is started
    engine->initPty();
    // send window resize
    int charWidth = gr_ttf_measureEx("N", mFont->GetResource());
    engine->setSize(mRenderW / charWidth, GetDisplayItemCount(), mRenderW, mRenderH);
}

void GUITerminal::SetPageFocus(int inFocus)
{
    if (inFocus && isConditionTrue()) {
        // TODO: grab focus correctly, this hack grabs focus and insists that the terminal be the focus regardless of other elements
        // It's highly unlikely that there will be any other visible input elements on the page anyway...
        SetInputFocus(1);
        InitAndResize();
    }
}
